{-# LANGUAGE CPP #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ImpredicativeTypes #-}
module Main (main) where

import Lens.Micro (Lens', (^.), (&), (%~))
import Lens.Micro.Mtl
import Control.Monad (void)
import Control.Monad.State (get)
import Control.Monad.IO.Class (liftIO)
#if !(MIN_VERSION_base(4,11,0))
import Data.Monoid
#endif
import qualified Graphics.Vty as V

import Brick.Types
  ( Widget
  , VScrollBarOrientation(..)
  , suffixLenses
  )
import Brick.Widgets.Core
  ( Padding(..)
  , str
  , txt
  , txtWrap
  , vLimit
  , hLimit
  , vBox
  , hBox
  , padLeft
  , padRight
  , padAll
  , fill
  , withDefAttr
  , withVScrollBars
  , (<+>)
  )
import Brick.Util
  ( on
  , fg
  )
import Brick.Forms
  ( Form
  , newForm
  , editTextField
  , checkboxField
  , radioField
  , renderForm
  , formState
  , handleFormEvent
  , formAttr
  , focusedFormInputAttr
  , invalidFormInputAttr
  , (@@=)
  )
import qualified Brick.Main as M
import qualified Brick.Types as T
import qualified Brick.Widgets.Border as B
import qualified Brick.Widgets.List as L
import qualified Brick.Widgets.Center as C
import qualified Brick.Widgets.Edit as E
import qualified Brick.Widgets.Dialog as D
import qualified Brick.AttrMap as A
import qualified Brick.Focus as F
import qualified Data.Vector as Vec
import qualified Data.Text as Text
import qualified Data.Time as Time

import qualified DataType as DT
import qualified CheckboxList as CL
import Persistence
  ( appDataFilePath
  , loadAppData
  , dumpAppData
  )
import Time
  ( getCurrentLocalTime
  , showMaybeTime
  , timeEditField
  )
import Timeline
  ( drawStatisticBoard
  )

data WidgetName = DoList
                | ScheduleList
                | DelegateList
                | EliminateList
                | TodoRadio
                | DoneRadio
                | NameEditor
                | DescEditor
                | UrgentCheckbox
                | ImportantCheckbox
                | ScheduleTimeEditor
                | DeadlineTimeEditor
                | SubTaskEditor Int
                | SubTaskList
                | ConfirmButton
                | CancelButton
                deriving (Show, Eq, Ord)

data Choice = Confirm
            | Cancel
            deriving (Show, Eq, Ord)

data WindowState = MainWindow
                 | TaskCreateForm
                 | TaskEditForm WidgetName Int
                 | ConfirmDeleteDialog WidgetName Int
                 deriving (Show, Eq, Ord)

data AppState =
    AppState { doList :: L.List WidgetName DT.Task
             , scheduleList :: L.List WidgetName DT.Task
             , delegateList :: L.List WidgetName DT.Task
             , eliminateList :: L.List WidgetName DT.Task
             , taskForm :: Form DT.Task String WidgetName
             , confirmDialog :: D.Dialog Choice WidgetName
             , focusRing :: F.FocusRing WidgetName
             , windowState :: WindowState
             , currentTime :: Time.LocalTime
             }

suffixLenses ''AppState

drawQuadrant :: AppState -> L.List WidgetName DT.Task -> Widget WidgetName
drawQuadrant s l = box where
  box = hLimit totalWidth $
    vLimit 20 $
    withVScrollBars OnRight $
    L.renderList listDrawElement focus l
  focus = s^.windowStateL == MainWindow &&
          Just (L.listName l) == F.focusGetCurrent (s^.focusRingL)

quadrantTitle :: String -> L.List WidgetName DT.Task -> Widget WidgetName
quadrantTitle t l = str $ t ++ "(" ++ done ++ "/" ++ total ++ ")" where
  els = L.listElements l
  total = show $ Vec.length els
  done = show . Vec.length $ Vec.filter _filter els
  _filter task = task^.DT.taskStatusL == DT.Done

drawQuadrantBoard :: AppState -> Widget WidgetName
drawQuadrantBoard s = ui where
  ui = vBox [ hBox [ B.borderWithLabel (quadrantTitle "Do" (s^.doListL)) $
                     drawQuadrant s (s^.doListL)
                   , B.borderWithLabel (quadrantTitle "Schedule" (s^.scheduleListL)) $
                     drawQuadrant s (s^.scheduleListL)
                   ]
            , hBox [ B.borderWithLabel (quadrantTitle "Delegate" (s^.delegateListL)) $
                     drawQuadrant s (s^.delegateListL)
                   , B.borderWithLabel (quadrantTitle "Eliminate" (s^.eliminateListL)) $
                     drawQuadrant s (s^.eliminateListL)
                   ]
            ]

makeForm :: Time.LocalTime -> DT.Task -> Form DT.Task e WidgetName
makeForm t =
  newForm [ label "Status" @@= radioField DT.taskStatusL
            [ (DT.Todo, TodoRadio, "Todo")
            , (DT.Done, DoneRadio, "Done")
            ]
          , label "Name" @@= editTextField DT.nameL NameEditor (Just 1)
          , label "Desc" @@= vLimit 3 @@= editTextField DT.descL DescEditor Nothing
          , label "Urgent" @@= checkboxField DT.urgentL UrgentCheckbox ""
          , label "Important" @@= checkboxField DT.importantL ImportantCheckbox ""
          , label "Schedule" @@= timeEditField DT.scheduleAtL ScheduleTimeEditor t showMaybeTime
          , label "Deadline" @@= timeEditField DT.deadlineAtL DeadlineTimeEditor t showMaybeTime
          , subTaskAugment @@= CL.checkboxListField DT.subTasksL SubTaskList SubTaskEditor
          ] where
  label s w = (vLimit 1 $ hLimit 10 $ str s <+> fill ' ') <+> w
  subTaskHead w = vBox [ B.hBorderWithLabel (txt "Sub Task List"), w ]
  subTaskAugment = subTaskHead . vLimit 5 . withVScrollBars OnRight

drawTaskForm :: AppState -> String -> Widget WidgetName
drawTaskForm s t = ui where
  label = str t
  ui = C.centerLayer $
       B.borderWithLabel label $
       hLimit 50 $
       vBox $ [ renderForm $ s^.taskFormL
              ]

makeDialog :: String -> D.Dialog Choice WidgetName
makeDialog t =
  D.dialog (Just $ str t) (Just (ConfirmButton, choices)) 50
  where
    choices = [ ("confirm", ConfirmButton, Confirm)
              , ("cancel", CancelButton, Cancel)
              ]

drawConfirmDialog :: AppState -> String -> Widget WidgetName
drawConfirmDialog s hint = ui where
  ui = D.renderDialog (s^.confirmDialogL) $
       C.hCenter $
       padAll 1 $
       str hint

drawKeyBind :: Widget WidgetName
drawKeyBind = box where
  box = B.border $ vBox [ C.hCenter $ str "Press +/- to add/remove list elements."
                        , C.hCenter $ str "Use arrow keys to change selection."
                        , C.hCenter $ str "Press Esc to exit."
                        ]

drawUI :: AppState -> [Widget WidgetName]
drawUI s =
  case s^.windowStateL of
    MainWindow -> [mainWindow]
    TaskCreateForm -> [drawTaskForm s "Create new task", mainWindow]
    TaskEditForm _ _ -> [drawTaskForm s "Edit task", mainWindow]
    ConfirmDeleteDialog _ _ -> [drawConfirmDialog s "Are you sure to delete the task?", mainWindow]
  where
    label = str "Agenda"
    mainLayout = vLimit 100 $
                 hBox [ drawQuadrantBoard s
                      , B.border $
                        drawStatisticBoard (s^.currentTimeL) $ Vec.toList $ (s^.doListL^.L.listElementsL)
                      ]
    mainWindow = B.borderWithLabel label $
                 vBox [ mainLayout
                      , drawKeyBind
                      ]

quadrantNameToGetter :: WidgetName
                     -> Lens' AppState (L.List WidgetName DT.Task)
quadrantNameToGetter n =  case n of
  DoList -> doListL
  ScheduleList -> scheduleListL
  DelegateList -> delegateListL
  EliminateList -> eliminateListL
  _ -> undefined

appEventEditTask :: T.EventM WidgetName AppState ()
appEventEditTask = do
  r <- use focusRingL
  case F.focusGetCurrent r of
    Nothing -> return ()
    Just f -> do
      let g = quadrantNameToGetter f
      sel <- use ((quadrantNameToGetter f).L.listSelectedL)
      case sel of
        Nothing -> return ()
        Just idx -> do
          els <- use (g.L.listElementsL)
          t <- liftIO getCurrentLocalTime
          taskFormL .= makeForm t (els Vec.! idx)
          windowStateL .= TaskEditForm f idx

taskTemplate :: WidgetName -> DT.Task
taskTemplate n = case n of
  DoList -> DT.withPriority True True DT.emptyTask
  ScheduleList -> DT.withPriority False True DT.emptyTask
  DelegateList -> DT.withPriority True False DT.emptyTask
  _ -> DT.withPriority False False DT.emptyTask

appEventCreateTask :: T.EventM WidgetName AppState ()
appEventCreateTask = do
  r <- use focusRingL
  case F.focusGetCurrent r of
    Nothing -> return ()
    Just f -> do
      t <- liftIO getCurrentLocalTime
      taskFormL .= makeForm t (taskTemplate f)
      windowStateL .= TaskCreateForm

appEventDeleteTask :: T.EventM WidgetName AppState ()
appEventDeleteTask = do
  r <- use focusRingL
  case F.focusGetCurrent r of
    Nothing -> return ()
    Just wn -> do
      sel <- use ((quadrantNameToGetter wn).L.listSelectedL)
      case sel of
        Nothing -> return ()
        Just idx -> do
          windowStateL .= ConfirmDeleteDialog wn idx

appEventSetTaskStatus :: DT.TaskStatus -> T.EventM WidgetName AppState ()
appEventSetTaskStatus s = do
  r <- use focusRingL
  case F.focusGetCurrent r of
    Nothing -> return ()
    Just wn -> do
      sel <- use ((quadrantNameToGetter wn).L.listSelectedL)
      case sel of
        Nothing -> return ()
        Just idx -> do
          els <- use ((quadrantNameToGetter wn).L.listElementsL)
          let task = els Vec.! idx
              task' = task { DT.taskStatus = s }
          quadrantNameToGetter wn %= L.listInsert idx task' . L.listRemove idx
          quadrantNameToGetter wn %= L.listMoveTo idx
          -- save app data to file
          appState <- get
          liftIO . dumpAppData appDataFilePath . gatherAppData $ appState

appEventMainWindow :: T.BrickEvent WidgetName e -> T.EventM WidgetName AppState ()
appEventMainWindow (T.VtyEvent e) =
  case e of
    V.EvKey (V.KChar '+') [] -> appEventCreateTask
    V.EvKey (V.KChar '-') [] -> appEventDeleteTask
    V.EvKey (V.KChar 't') [] -> appEventSetTaskStatus DT.Todo
    V.EvKey (V.KChar 'd') [] -> appEventSetTaskStatus DT.Done
    V.EvKey V.KEnter [] -> appEventEditTask
    V.EvKey (V.KChar '\t') [] -> focusRingL %= F.focusNext
    V.EvKey V.KBackTab [] -> focusRingL %= F.focusPrev
    -- switch to quadrant with number
    V.EvKey (V.KChar '1') [] -> focusRingL %= F.focusSetCurrent DoList
    V.EvKey (V.KChar '2') [] -> focusRingL %= F.focusSetCurrent ScheduleList
    V.EvKey (V.KChar '3') [] -> focusRingL %= F.focusSetCurrent DelegateList
    V.EvKey (V.KChar '4') [] -> focusRingL %= F.focusSetCurrent EliminateList
    V.EvKey V.KEsc [] -> M.halt
    other -> do
      r <- use focusRingL
      case F.focusGetCurrent r of
        Just DoList -> T.zoom doListL $ L.handleListEvent other
        Just ScheduleList -> T.zoom scheduleListL $ L.handleListEvent other
        Just DelegateList -> T.zoom delegateListL $ L.handleListEvent other
        Just EliminateList -> T.zoom eliminateListL $ L.handleListEvent other
        _ -> return ()
appEventMainWindow _ = return ()

taskToQuadrant :: DT.Task
               -> WidgetName
taskToQuadrant task
  | u && c = DoList
  | not u && c = ScheduleList
  | u && not c = DelegateList
  | not u && not c = EliminateList
  | otherwise = undefined
  where
    u = task^.DT.urgentL
    c = task^.DT.importantL

focusAndSelectTask :: WidgetName -> Int -> T.EventM WidgetName AppState ()
focusAndSelectTask wn idx = do
  focusRingL %= F.focusSetCurrent wn
  quadrantNameToGetter wn %= L.listMoveTo idx

appEventFormCommit :: T.EventM WidgetName AppState ()
appEventFormCommit = do
  f <- use taskFormL
  let task = formState f
      g = quadrantNameToGetter (taskToQuadrant task)
      subTaskFilter subTask = not . Text.null . Text.strip $ subTask^.DT.subNameL
      -- Drop empty sub tasks
      task' = task & DT.subTasksL %~ Vec.filter subTaskFilter
  els <- use (g.L.listElementsL)
  ws <- use windowStateL
  case ws of
    TaskEditForm w idx -> do
      let g' = quadrantNameToGetter w
      els' <- use (g'.L.listElementsL)
      let origin = els' Vec.! idx
      if taskToQuadrant task == taskToQuadrant origin
        then do
        quadrantNameToGetter (taskToQuadrant task) %= L.listInsert idx task' . L.listRemove idx
        focusAndSelectTask (taskToQuadrant task) idx
        else do
        quadrantNameToGetter w %= L.listRemove idx
        quadrantNameToGetter (taskToQuadrant task) %= L.listInsert (Vec.length els) task'
        focusAndSelectTask (taskToQuadrant task) (Vec.length els)
    _ -> do
      quadrantNameToGetter (taskToQuadrant task) %= L.listInsert (Vec.length els) task'
      focusAndSelectTask (taskToQuadrant task) (Vec.length els)
  -- save app data to file
  s <- get
  liftIO . dumpAppData appDataFilePath . gatherAppData $ s

appEventTaskForm :: T.BrickEvent WidgetName e -> T.EventM WidgetName AppState ()
appEventTaskForm (T.VtyEvent e) = do
  case e of
    V.EvKey V.KEsc [] -> windowStateL .= MainWindow
    V.EvKey (V.KChar 's') [V.MCtrl] ->
      appEventFormCommit >> windowStateL .= MainWindow
    _ -> T.zoom taskFormL $ handleFormEvent (T.VtyEvent e)
appEventTaskForm _ = return ()

appEventConfirmDelete :: T.EventM WidgetName AppState ()
appEventConfirmDelete = do
  d <- use confirmDialogL
  case D.dialogSelection d of
    Nothing -> return ()
    Just (_, c) -> do
      case c of
        Cancel -> return ()
        Confirm -> do
          ws <- use windowStateL
          let (ConfirmDeleteDialog wn idx) = ws
          quadrantNameToGetter wn %= L.listRemove idx
          -- save app data to file
          s <- get
          liftIO . dumpAppData appDataFilePath . gatherAppData $ s

appEventConfirmDialog :: T.BrickEvent WidgetName e -> T.EventM WidgetName AppState ()
appEventConfirmDialog (T.VtyEvent e) =
  case e of
    V.EvKey V.KEsc [] -> windowStateL .= MainWindow
    V.EvKey V.KEnter [] -> appEventConfirmDelete >> windowStateL .= MainWindow
    _ -> T.zoom confirmDialogL $ D.handleDialogEvent e
appEventConfirmDialog _ = return ()

appEvent :: T.BrickEvent WidgetName e -> T.EventM WidgetName AppState ()
appEvent e = do
  ws <- use windowStateL
  case ws of
    MainWindow -> appEventMainWindow e
    TaskCreateForm -> appEventTaskForm e
    TaskEditForm _ _ -> appEventTaskForm e
    ConfirmDeleteDialog _ _ -> appEventConfirmDialog e

listDrawDuration :: DT.Task -> Widget WidgetName
listDrawDuration task =
  let schedule = task^.DT.scheduleAtL
      deadline = task^.DT.deadlineAtL
  in str $ case (schedule, deadline) of
    (Just s, Just d) -> "from " <> show s <> " to " <> show d
    (Just s, Nothing) -> "schedule " <> show s
    (Nothing, Just d) -> "deadline " <> show d
    _ -> ""

listDrawElement :: Bool -> DT.Task -> Widget WidgetName
listDrawElement _ task = item where
  status = task^.DT.taskStatusL
  statusLabelAttr =
    if status == DT.Todo
    then todoStatusLabelAttr
    else doneStatusLabelAttr
  drawSubTask (DT.SubTask c d) =
    hBox [ txt $ if c then "- [X] " else "- [ ] "
         , txt d
         ]
  subTaskItems = drawSubTask <$> task^.DT.subTasksL
  header = hBox [ withDefAttr statusLabelAttr $ str (show status)
                , padLeft Max $ listDrawDuration task
                ]
  item = hLimit totalWidth $
         padLeft (Pad 1) $
         padRight (Pad 1) $
         vBox $ [ header
                , txtWrap $ task^.DT.nameL
                , txtWrap $ task^.DT.descL
                ] <> Vec.toList subTaskItems

mainWindowFocusRing :: F.FocusRing WidgetName
mainWindowFocusRing =
  F.focusRing [ DoList
              , ScheduleList
              , DelegateList
              , EliminateList
              ]

todoStatusLabelAttr :: A.AttrName
todoStatusLabelAttr = A.attrName "todoStatusLabelAttr"

doneStatusLabelAttr :: A.AttrName
doneStatusLabelAttr = A.attrName "doneStatusLabelAttr"

grey :: V.Color
grey = V.color240 50 50 (50 :: Int)

theMap :: A.AttrMap
theMap = A.attrMap V.defAttr
    [ (L.listAttr, V.white `on` V.black)
    , (L.listSelectedAttr, V.white `on` grey)
    , (L.listSelectedFocusedAttr, V.black `on` V.yellow)
    , (E.editAttr, V.white `on` V.black)
    , (E.editFocusedAttr, V.black `on` V.yellow)
    , (formAttr, V.white `on` V.black)
    , (focusedFormInputAttr, V.black `on` V.yellow)
    , (invalidFormInputAttr, V.black `on` V.red)
    , (D.dialogAttr, V.yellow `on` V.black)
    , (D.buttonAttr, V.white `on` V.black)
    , (D.buttonSelectedAttr, V.black `on` V.yellow)
    , (todoStatusLabelAttr, fg V.red)
    , (doneStatusLabelAttr, fg V.green)
    , (CL.listAttr, V.white `on` V.black)
    , (CL.listSelectedAttr, V.white `on` grey)
    , (CL.listSelectedFocusedAttr, V.black `on` V.yellow)
    ]

columnWidths :: [Int]
columnWidths = [8, 30, 30]

totalWidth :: Int
totalWidth = sum columnWidths

theApp :: M.App AppState e WidgetName
theApp =
    M.App { M.appDraw = drawUI
          , M.appChooseCursor = M.showFirstCursor
          , M.appHandleEvent = appEvent
          , M.appStartEvent = return ()
          , M.appAttrMap = const theMap
          }

initAppData :: IO DT.AppData
initAppData = do
  md <- loadAppData appDataFilePath
  return $ maybe DT.emptyAppData id md

initAppState :: Time.LocalTime -> DT.AppData -> AppState
initAppState t d =
  AppState { doList = L.list DoList (d^.DT.doTasksL) 1
           , scheduleList = L.list ScheduleList (d^.DT.scheduleTasksL) 1
           , delegateList = L.list DelegateList (d^.DT.delegateTasksL) 1
           , eliminateList = L.list EliminateList (d^.DT.eliminateTasksL) 1
           , taskForm = makeForm t DT.emptyTask
           , confirmDialog = makeDialog "Confirm dialog"
           , focusRing = mainWindowFocusRing
           , windowState = MainWindow
           , currentTime = t
           }

gatherAppData :: AppState -> DT.AppData
gatherAppData s =
  DT.AppData { DT.doTasks = L.listElements (s^.doListL)
             , DT.scheduleTasks = L.listElements (s^.scheduleListL)
             , DT.delegateTasks = L.listElements (s^.delegateListL)
             , DT.eliminateTasks = L.listElements (s^.eliminateListL)
             }

main :: IO ()
main = do
  t <- getCurrentLocalTime
  d <- initAppData
  void $ M.defaultMain theApp (initAppState t d)
